X

曜彤.手记

随记,关于互联网技术、产品与创业

  1. Chapter 1: The New C++17 Features
    1. 结构化绑定(Structured Bindings / Unpacking)
    2. if 与 switch 的语句内初始化(语法糖)
    3. 新的括号初始化规则
    4. 基于构造函数的自动模板类型推导(CTAD)
    5. constexpr-if 的编译时决策
    6. 全局变量内联
    7. 折叠表达式
  • Chapter 2: STL Containers
    1. std::vector 与 Erase-remove 惯用法
    2. 从未排序的 std::vector 中快速删除元素
    3. 以最快 / 最安全的方式访问 std::vector / std::array
    4. 保持 std::vector / std::array 以有序的方式插入元素
    5. 高效和选择性地插入元素到 std::map / std::unordered_map
    6. std::map::insert 的新插入提示语义(性能优化)
    7. 高效地修改 std::map 中元素的 Key
    8. 在 std::unordered_map 中使用自定义类型做 Key
    9. 使用 std::set 过滤重复的用户输入并排序
    10. 使用 std::priority_queue 编写 TODO 应用
  • 《C++17 STL Cookbook》读书笔记(第 1-2 章)

    看到知乎上有人推荐的一本书。

    Chapter 1: The New C++17 Features

    结构化绑定(Structured Bindings / Unpacking)
    std::pair<int, int> map(int x, int y) {
      return { x, y };
    }
    struct City {
      int id;
      std::string label;
    };
    int main(int argc, char **argv) {
      // std::pair.
      const auto [x, y] = (map(10, 20));
      std::cout << x << y << std::endl;
    
      // std::tuple.
      const auto [age, name] = std::make_tuple<>(27, std::string("YHSPY"));
      std::cout << name << age << std::endl;
    
      // struct (with reference).
      City city = { 10, "Shanghai" };
      const auto &[id, label] = city;
      std::cout << id << label << std::endl;
    
      // array.
      int arr[]= {1, 2};
      const auto [first, second] = arr;
    
      // std::array.
      auto [m, n] = std::array<int, 2>({10, 20});
      return 0;
    }
    
    • 可用于 std::pair \ std::tuple \ struct(只能用于非静态成员)\ array;
    • std::tuple 与 C-struct 的结构类似,可以有引用类型的成员;而 std::vector 等则需要使用 std::reference_wrapper 将引用类型包装成对象的形式来存放;
    • std::make_tuple 在默认情况下会将 T&& 退化到 T

    For each Ti in Types…, the corresponding type Vi in VTypes… is std::decay<Ti>::type unless application of std::decay results in std::reference_wrapper<X> for some type X, in which case the deduced type is X&.

    int main(int argc, char **argv) {
      int x {};
      int y {};
      /**
       * template< class T, class... Types >
       * constexpr T&& get(tuple<Types...>&& t) noexcept;
       */
      // decay.
      const auto [v] = std::make_tuple<int&&>(std::move(x));
      static_assert(std::is_same_v<decltype(v), const int>);
      // no decay.
      const auto [c] = std::tuple<int&&>(std::move(y));
      static_assert(std::is_same_v<decltype(c), int&&>);
      return 0;
    }
    
    • 在 “[const|volatile][T][&|&&][identifier]” 形式的表达式中,当其中的 [&|&&] 引用部分存在时,[const|volatile] 将不属于 decltype([identifier]) 所推断类型的一部分(引用本身无法被 rebind,因此也就没有 constness 的概念,一旦初始化不会再被改变)。区分 const T& 与 T& const。总结来说,具有 copyable 属性的值或对象(普通值 \ 指针等)在传递时都会忽略 top-level constness;
    • 可以使用 std::tie 来实现类似的 Unpacking,其一个优势在于对于不需要的字段可以通过 std::ignore 的方式将其丢弃。而在 Structured-Bindings 中,则需要为容器中的每一个字段都提供相应的绑定变量;
    • 结构化绑定也是一种为已存在对象添加的别名,但一个结构化绑定不一定使用引用类型来实现
    const auto [v] = std::tuple<int&>(x);
    /**
     * 等价于:
     * const auto e = std::tuple<int&>(x);  // const -> std::tuple;
     * auto&& r = std::get<0>(std::move(e));  // auto&& T -> &/&&;
     * (introduce v as a name for that which r refers to)
     */
    
    if 与 switch 的语句内初始化(语法糖)
    int main(int argc, char **argv) {
      if (int x = 10; x == 10) {
        std::cout << "Yes!" << std::endl;
      }
      /*
          {
            int x = 10;
            if (x == 10) {
              std::cout << "Yes!" << std::endl;
            }
          }
       */
      switch(int y = 10; y) {
        case 10: {
          std::cout << "Yes again!" << std::endl;
        }
        default: {}
      }
      return 0;
    }
    

    应用场景

    • 互斥锁相关,比如 std::lock_guard<std::mutex>。锁设置成功后执行 scope 内语句,退出 scope 时,锁被自动释放;
    • 通过 std::weak_ptr::lock 构建的可能处于不可用状态的 std::shared_ptr;
    • 以函数参数形式传递返回值的一些历史遗留函数;
    新的括号初始化规则
    int main(int argc, char **argv) {
      // "x" should be the type of the only element inside the brackets in C++17.
      auto x {1}; 
      auto y = {1, 2, 3};  // y -> std::initializer_list.
      // auto z {1, 2}; // illegal.
      std::cout << typeid(x).name() << std::endl;
      return 0;
    }
    
    基于构造函数的自动模板类型推导(CTAD)
    int main(int argc, char **argv) {
      std::pair city(1, "Shanghai");
      std::tuple scores(4, 3, 2.5);
      
      return 0;
    }
    

    使用 std::common_type_t 让编译器选择合适的推导类型。

    template <typename T>
    struct sum {
        T value;
        template <typename ...Ts>
        sum(Ts&& ...values) : value{(values + ...)} {}
    };
    
    template <typename ...Ts>
    sum(Ts&& ...ts) -> sum<std::common_type_t<Ts...>>;
    int main(int argc, char **argv) {
      sum s {1u, 2.0, 3, 4.0f};
      std::cout << s.value << std::endl;  // 10.
      return 0;
    }
    
    constexpr-if 的编译时决策
    template <typename T>
    class Addable { 
      T val;
    public:
      Addable(T v) : val{v} {}
      template <typename U>
      T add(U x) const {
        if constexpr (std::is_same_v<T, std::vector<U>>) {  // evaluting at compile-time.
          auto copy(val);
          for (auto &n : copy) { 
            n += x;
          }
          return copy;
        } else {
          return val + x;
        }
      }
    };
    int main(int argc, char **argv) {
      Addable addable {std::vector<int>{1, 2, 3}};
      for (const auto& v : addable.add(100)) {
        std::cout << v << std::endl;
      }
      return 0;
    }
    
    • 在 C++17 之前,需要借助 std::enable_if_t<condition, type> 基于 SFINAE 来支持诸如返回值类型选择等过程;
    全局变量内联
    struct A {
      static const inline std::string name = "YHSPY";
    };
    inline A a;
    int main(int argc, char **argv) {
      std::cout << a.name << std::endl;
      return 0;
    }
    
    • 可用于生成 “Header-Only”(不需要先将 .h 与 .cc 编译成 .o 然后再链接的过程)的 C++ 库;
    • ODR-use(One Definition Rule);
    • 对于标记为 inline 的全局变量,编译器在链接时会选择使用第一次出现的符号(编译器会假设其他符号与该符号的定义相同);
    折叠表达式

    可用于方便地对变长参数模板进行参数解包

    // before C++11:
    template<typename ...T> double mul(T ...args);  // 入口;
    template<typename S, typename ...T> double mul(S arg, T ...args) { return arg * mul<T...>(args...); }  // 递归循环体;
    template<> double mul() { return 1; }  // 边界条件(全特化);
    // after C++17:
    template<typename ...Ts> double mulByFold(Ts ...args) {
      return (args * ... * 1);  // 折叠表达式(二元折叠);
    }
    int main(int argc, char **argv) {
      std::cout << mul<>(1, 2, 3, 4) << std::endl;  // 24.
      std::cout << mulByFold<>(1, 2, 3, 4) << std::endl;  // 24.
      return 0;
    }
    

    场景 1:查询给定参数在容器中总共出现的次数:

    template <typename R, typename ...Ts>
    auto matches(const R& range, Ts ...ts) {
      return (std::count(std::begin(range), std::end(range), ts) + ...);
    }
    int main(int argc, char **argv) {
      std::cout << matches(std::vector{1, 2, 2, 3, 4, 5}, 2, 5); 
      return 0;
    }
    

    场景 2:检查顺序的批量元素容器插入操作是否成功:

    template <typename T, typename ...Ts>
    bool insertAllToSet(T&& set, Ts ...ts) {
      return (set.insert(ts).second && ...);  // short-circuit.
    }
    int main(int argc, char **argv) {
      std::cout << std::boolalpha << insertAllToSet(std::set<int>{1, 2}, 3, 4, 5);
      return 0;
    }
    

    场景 3:检查是否所有元素均在给定的范围内:

    template <typename T, typename ...Ts>
    bool within(T min, T max, Ts ...ts) {
      return ((min <= ts && ts <= max) && ...);
    }
    int main(int argc, char **argv) {
      std::cout << std::boolalpha << within(1, 10, 1.3, 2, 3);
      return 0;
    }
    

    场景 4:批量向容器插入元素:

    template <typename T, typename ...Ts>
    void insertAllToVector(std::vector<T>& vec, Ts ...ts) {
      (vec.push_back(ts), ...);  // comma expression.
    }
    int main(int argc, char **argv) {
      std::vector<int> vec {};
      insertAllToVector(vec, 1, 2, 3);
      std::cout << vec.size() << std::endl;
      return 0;
    }
    

    Chapter 2: STL Containers

    • std::vector 使用了堆内存,以提供可随时增长容器大小的特性;
    • std::unordered_setstd::unordered_set 基于 Hash Table 实现;
    • 容器适配器:std::stack \ std::queue 以及 std::priority_queue
    std::vector 与 Erase-remove 惯用法
    • 从线性容器(std::vector \ std::array \ std::string)中删除元素时采用的常用模式
    • 应用在 std::array 时,注意不支持对容器大小的更改;

    int main(int argc, char **argv) {
      std::vector v {1, 2, 3, 2, 5, 2, 6, 2, 4, 8};
      const auto newEnd (std::remove(std::begin(v),std::end(v), 2));  // remove.
      v.erase(newEnd, end(v));  // erase.
    
      // predicate version:
      const auto odd ([](int i) { return i % 2 != 0; });
      v.erase(std::remove_if(std::begin(v), std::end(v), odd), std::end(v));  // remove && erase.
      v.shrink_to_fit();  // shrink capacity of the container.
      for (const auto& i : v) { std::cout << i << std::endl; }
      return 0;
    }
    
    从未排序的 std::vector 中快速删除元素
    • 基本思路:用容器中的最后一个元素替换待删除元素;然后将最后一个元素移除(省去了需要大量移动元素的开销,但只适合无序容器)。
    // index version:
    template <typename T>
    void quickRemoveAt(std::vector<T>& v, std::size_t idx) {
      if (idx < v.size()) {
        v[idx] = std::move(v.back());  // use std::move rather than copy.
        v.pop_back();  // discard the last item.
      }
    }
    // iterator version:
    template <typename T>
    void quickRemoveAt(std::vector<T>& v, typename std::vector<T>::iterator it) {
      if (it != std::end(v)) {
        *it = std::move(v.back());
        v.pop_back();
      }
    }
    int main(int argc, char **argv) {
      std::vector vec {1, 2, 3};
      quickRemoveAt(vec, 1);
      for (const auto& v : vec) {
        std::cout << v << std::endl;
      }
      return 0;
    }
    
    以最快 / 最安全的方式访问 std::vector / std::array
    • 下标运算符 “[]” 不会进行 bound-check,相反 at() 则会进行 bound-check(在某些情况下也可能会产生性能损耗);
    • 可以通过捕获 std::out_of_range 异常来处理容器元素访问 out-of-bound 的情况;
    保持 std::vector / std::array 以有序的方式插入元素
    void insertSorted(std::vector<int>& v, int num) {
      // find the first element which is the greater or equal than the given one.
      const auto insertPos(std::lower_bound(std::begin(v), std::end(v), num));  
      // insert the new element just behind this one.
      v.insert(insertPos, num);
    }
    int main(int argc, char **argv) {
      std::vector vec {1, 2, 3, 4, 5, 6};
      insertSorted(vec, 2);
      assert(true == std::is_sorted(std::begin(vec), std::end(vec)));
      return 0;
    }
    
    高效和选择性地插入元素到 std::map / std::unordered_map
    • 主要利用 C++17 新加入的 std::map::try_emplace。该方法在对应 Key 存在时不会尝试重新构建对象(在某种程度上可以节省开销);
    int main(int argc, char **argv) {
      std::map<int, std::string> m { { 1, "YHSPY" }, { 2, "SKY" } };
      auto [iter, success] = m.try_emplace(2, "SUN");
      if (!success) {
        iter->second = "SUM";
      }
      for (const auto& [key, value] : m) {
        std::cout << value << std::endl;
      }
      return 0;
    }
    
    std::map::insert 的新插入提示语义(性能优化)
    • 正常情况下,在 std::map 中查询/插入一个元素的时间复杂度为 ***O(log(n))***;在 hint 的帮助下,可以优化到 ***O(1)***;
    • 在 C++11 之前,插入值需要位于 hint 的后面;在 C++11 之后,插入值要尽可能接近 hint,并位于其前面。(*Inserts value in the position as close as possible, just prior(since C++11), to hint.*)
    • 使用“红黑树”实现的 std::map 在每次插入新元素时都会进行 re-balancing 的过程,该过程会牺牲一定的性能。而借助 hint 可以一定程度减轻此过程的影响(Amortized Complexity)。

    int main(int argc, char **argv) {
      std::map<std::string, size_t> m { { "b", 1 }, { "c", 2 }, { "d", 3 } };
      auto insertIt(std::end(m));
      for (const auto &s : { "z", "y", "x", "w" }) {
        // hint (point to the existing element, which is greater than the element to be inserted).
        insertIt = m.insert(insertIt, { s, 1 });
      }
      return 0;
    }
    
    高效地修改 std::map 中元素的 Key
    • std::map 中的 Key 部分是隐式 const 的,即无法在元素被插入后再进行修改。在 C++17 之前,只能够通过“先移除,再插入”的方式来间接修改;
    • std::map::extract 不会移动或者拷贝 std::map 中的元素,仅涉及修改对应元素的内部指针。(可能会有 re-balancing 的过程被执行)。
    • std::map::extract 返回的“节点”可以在其他同类型的容器中相互使用,但需要保证节点的 Key、Value 以及分配器类型(堆\栈)均相同
    int main(int argc, char **argv) {
      std::map<int, std::string> m {
        { 1, "Mario" }, { 2, "Luigi" }
      };
      auto a (m.extract(1));  // extract the target node.
      auto b (m.extract(2));
      if (!a.empty() && !b.empty()) {
        std::swap(a.key(), b.key());  // get nonconst access to the key.
        m.insert(std::move(a));  // no "move" or "copy", just adjust the internal pointer of each "node_type";
        m.insert(std::move(b));
        for (const auto& [key, value] : m) {
          std::cout << key << value << std::endl;
        }
      }
      return 0;
    }
    
    在 std::unordered_map 中使用自定义类型做 Key
    struct Name {
      std::string val;
    };
    bool operator==(const Name& x, const Name& y) {
      return x.val == y.val;
    }
    template<> struct std::hash<Name> {  // specialization for std::hash<Name>;
      size_t operator()(const Name& ins) const {
        return std::hash<std::string>()(ins.val);
      }
    };
    int main(int argc, char **argv) {
      std::unordered_map<Name, int> um {
        {{"Alice"}, 1}, {{"YHSPY"}, 2}
      };
      return 0;
    }
    
    使用 std::set 过滤重复的用户输入并排序
    int main(int argc, char **argv) {
      std::set<std::string> s;
      std::istream_iterator<std::string> it { std::cin };
      std::istream_iterator<std::string> end;
      std::copy(it, end, std::inserter(s, s.end()));
      return 0;
    }
    
    使用 std::priority_queue 编写 TODO 应用
    • std::priority_queue 不支持基于 std::initializer_list 的构造函数
    • std::pair 的比较规则:If lhs.first<rhs.first, returns true. Otherwise, if rhs.first<lhs.first, returns false. Otherwise, if lhs.second<rhs.second, returns true. Otherwise, returns false.
    int main(int argc, char **argv) {
      using tItem = std::pair<int, std::string>;
      std::priority_queue<tItem> todo;
      auto il = { 
        tItem{0, "read comics"},
        tItem{2, "do homework"},
        tItem{1, "dishes"},
      };
      for (const auto& p : il) {
        todo.push(p);
      }
      while (!todo.empty()) {
        std::cout << todo.top().first << ": " << todo.top().second << std::endl;
        todo.pop();
      }
      return 0;
    }
    



    评论 | Comments


    Loading ...